8.2.1 log包

让我们从 log 包提供的最基本的功能开始,之后再学习如何创建定制的日志记录器。记录日志的目的是跟踪程序什么时候在什么位置做了什么。这就需要通过某些配置在每个日志项上要写的一些信息,如代码清单8-2所示。

代码清单8-2 跟踪日志的样例

TRACE: 2009/11/10 23:00:00.000000 /tmpfs/gosandbox-/prog.go:14: message

在代码清单8-2中,可以看到一个由 log 包产生的日志项。这个日志项包含前缀、日期时间戳、该日志具体是由哪个源文件记录的、源文件记录日志所在行,最后是日志消息。让我们看一下如何配置 log 包来输出这样的日志项,如代码清单8-3所示。

代码清单8-3 listing03.go

01 // 这个示例程序展示如何使用最基本的log包
02 package main
03
04 import (
05   "log"
06 )
07
08 func init() {
09   log.SetPrefix("TRACE: ")
10   log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
11 }
12
13 func main() {
14   // Println写到标准日志记录器
15   log.Println("message")
16
17   // Fatalln在调用Println()之后会接着调用os.Exit(1)
18   log.Fatalln("fatal message")
19
20   // Panicln在调用Println()之后会接着调用panic()
21   log.Panicln("panic message")
22 }

如果执行代码清单8-3中的程序,输出的结果会和代码清单8-2所示的输出类似。让我们分析一下代码清单8-4中的代码,看看它是如何工作的。

代码清单8-4 listing03.go:第08行到第11行

08 func init() {
09   log.SetPrefix("TRACE: ")
10   log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
11 }

在第08行到第11行,定义的函数名为 init() 。这个函数会在运行 main() 之前作为程序初始化的一部分执行。通常程序会在这个 init() 函数里配置日志参数,这样程序一开始就能使用 log 包进行正确的输出。在这段程序的第9行,设置了一个字符串,作为每个日志项的前缀。这个字符串应该是能让用户从一般的程序输出中分辨出日志的字符串。传统上这个字符串的字符会全部大写。

有几个和 log 包相关联的标志,这些标志用来控制可以写到每个日志项的其他信息。代码清单8-5展示了目前包含的所有标志。

代码清单8-5 golang.org/src/log/log.go

const (
 // 将下面的位使用或运算符连接在一起,可以控制要输出的信息。没有
 // 办法控制这些信息出现的顺序(下面会给出顺序)或者打印的格式
 // (格式在注释里描述)。这些项后面会有一个冒号:
 //  2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
 // 日期: 2009/01/23
 Ldate = 1 << iota
 // 时间: 01:23:23
 Ltime
 // 毫秒级时间: 01:23:23.123123。该设置会覆盖Ltime标志
 Lmicroseconds
 // 完整路径的文件名和行号: /a/b/c/d.go:23
 Llongfile
 // 最终的文件名元素和行号: d.go:23
 // 覆盖 Llongfile
 Lshortfile
 // 标准日志记录器的初始值
 LstdFlags = Ldate | Ltime
)

代码清单8-5是从 log 包里直接摘抄的源代码。这些标志被声明为常量,这个代码块中的第一个常量叫作 Ldate ,使用了特殊的语法来声明,如代码清单8-6所示。

代码清单8-6 声明 Ldate 常量

// 日期: 2009/01/23
Ldate = 1 << iota

关键字 iota 在常量声明区里有特殊的作用。这个关键字让编译器为每个常量复制相同的表达式,直到声明区结束,或者遇到一个新的赋值语句。关键字 iota 的另一个功能是, iota 的初始值为0,之后 iota 的值在每次处理为常量后,都会自增1。让我们更仔细地看一下这个关键字,如代码清单8-7所示。

代码清单8-7 使用关键字 iota

const (
 Ldate = 1 << iota // 1 << 0 = 000000001 = 1
 Ltime        // 1 << 1 = 000000010 = 2
 Lmicroseconds   // 1 << 2 = 000000100 = 4
 Llongfile     // 1 << 3 = 000001000 = 8
 Lshortfile     // 1 << 4 = 000010000 = 16
 ...
)

代码清单8-7展示了常量声明背后的处理方法。操作符 << 对左边的操作数执行按位左移操作。在每个常量声明时,都将1按位左移 iota 个位置。最终的效果使为每个常量赋予一个独立位置的位,这正好是标志希望的工作方式。

常量 LstdFlags 展示了如何使用这些标志,如代码清单8-8所示。

代码清单8-8 声明 LstdFlags 常量

const (
 ...
 LstdFlags = Ldate(1) | Ltime(2) = 00000011 = 3
)

在代码清单8-8中看到,因为使用了复制操作符, LstdFlags 打破了 iota 常数链。由于有 | 运算符用于执行或操作,常量 LstdFlags 被赋值为3。对位进行或操作等同于将每个位置的位组合在一起,作为最终的值。如果对位1和2进行或操作,最终的结果就是3。

让我们看一下我们要如何设置日志标志,如代码清单8-9所示。

代码清单8-9 listing03.go:第08行到第11行

08 func init() {
09   ...
10   log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
11 }

这里我们将 LdateLmicrosecondsLlongfile 标志组合在一起,将该操作的值传入 SetFlags 函数。这些标志值组合在一起后,最终的值是 13 ,代表第1、3和4位为1(00001101)。由于每个常量表示单独一个位,这些标志经过或操作组合后的值,可以表示每个需要的日志参数。之后 log 包会按位检查这个传入的整数值,按照需求设置日志项记录的信息。

初始完 log 包后,可以看一下 main() 函数,看它是如何写消息的,如代码清单8-10所示。

代码清单8-10 listing03.go:第13行到第22行

13 func main() {
14   // Println写到标准日志记录器
15   log.Println("message")
16
17   // Fatalln在调用Println()之后会接着调用os.Exit(1)
18   log.Fatalln("fatal message")
19
20   // Panicln在调用Println()之后会接着调用panic()
21   log.Panicln("panic message")
22 }

代码清单8-10展示了如何使用3个函数 PrintlnFatallnPanicln 来写日志消息。这些函数也有可以格式化消息的版本,只需要用 f 替换结尾的 lnFatal 系列函数用来写日志消息,然后使用 os.Exit(1) 终止程序。 Panic 系列函数用来写日志消息,然后触发一个 panic 。除非程序执行 recover 函数,否则会导致程序打印调用栈后终止。 Print 系列函数是写日志消息的标准方法。

log 包有一个很方便的地方就是,这些日志记录器是多goroutine安全的。这意味着在多个goroutine可以同时调用来自同一个日志记录器的这些函数,而不会有彼此间的写冲突。标准日志记录器具有这一性质,用户定制的日志记录器也应该满足这一性质。

现在知道了如何使用和配置 log 包,让我们看一下如何创建一个定制的日志记录器,以便可以让不同等级的日志写到不同的目的地。

results matching ""

    No results matching ""